﻿/* 
 * Copyright (c) 2011-12 Andrey Bulygin
 * 
 * This file is part of Interceptor.
 * Interceptor is free software: you can redistribute it and/or modify it under the terms of the GNU 
 * Library General Public License as published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 * 
 * Interceptor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 
 * the GNU Library General Public License for more details. You should have received a copy of the GNU 
 * Library General Public License along with Interceptor. If not, write to the Free Software Foundation, Inc., 59 
 * Temple Place, Suite 330, Boston, MA 02111-1307 USA.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection.Emit;
using System.Text.RegularExpressions;
using System.Threading;
using System.Reflection;
using System.Linq.Expressions;
using Interceptor.Core.Objects;
using Interceptor.Core.Exceptions;

namespace Interceptor.Core
{
	internal class DecoratorFactory : BaseFactory
	{
		private const string SRC_FLDNAME = "_source";
		private const string ITC_FLDNAME = "_interceptor";

		public static Func<object, object, IDecorator> MakeInterceptorFactoryDelegate(object sourceInstance, Type interceptorType)
		{
			var source = TypeDescription.GetDescription(sourceInstance);
			var interceptor = ItcHolderDescriptor.GetDescription(interceptorType);
			return MakeDecoration(source, interceptor);
		}

		private static Func<object, object, IDecorator> MakeDecoration(TypeDescription source, ItcHolderDescriptor interceptor)
		{
			var typeContext = CreateType(source);

			typeContext.DefinePrivateField(SRC_FLDNAME, source.Definition);
			typeContext.DefinePrivateField(ITC_FLDNAME, interceptor.Definition);

			if (!source.AlreadyDecorated)
				ImplementIDecorator(source, typeContext);

			GenerateConstructor(typeContext);

			var prepareActions = new List<LambdaExpression>();

			foreach (var intface in source.Interfaces)
			{
				if (intface!=typeof(IDecorator))
				WrapInterface(source.TypeToFindMatches, interceptor, typeContext, intface, prepareActions);
				else
					WrapInterface(source.TypeToFindMatches, ItcHolderDescriptor.Empty, typeContext, intface, prepareActions);
			}
			Type type = typeContext.CompileType();

			foreach (var act in prepareActions)
				((Action)act.Compile())();

#if SAVE_TO_FILE
			typeContext.Assembly.Save("asm.dll");
#endif
			var par1 = LambdaExpression.Parameter(typeof(object));
			var par2 = LambdaExpression.Parameter(typeof(object));

			LambdaExpression exp = LambdaExpression.Lambda(
				typeof(Func<object, object, IDecorator>),
				LambdaExpression.New(type.GetConstructors()[0],
					LambdaExpression.Convert(par1, source.Definition),
					LambdaExpression.Convert(par2, interceptor.Definition)),
				new[] { par1, par2 });


			return (Func<object, object, IDecorator>)exp.Compile();
		}

		private static void ImplementIDecorator(TypeDescription source, TypeBuilderContext typeContext)
		{
			MethodInfo decoratorMethod = typeof(IDecorator).GetMethod("get_Target");

			typeContext.Override(decoratorMethod,
								 (type, il) =>
								 {
									 il.Emit(OpCodes.Ldarg_0);
									 il.Emit(OpCodes.Ldfld, type.Field(SRC_FLDNAME));
									 il.Emit(OpCodes.Ret);
								 });

		}

		private static void WrapInterface(Type typeToFindMatches, ItcHolderDescriptor itcHolder, TypeBuilderContext context, Type intface, List<LambdaExpression> prepareActions)
		{
			var properties = intface.GetProperties();

			var propMethods = new List<MethodInfo>();
			foreach (var prop in properties)
			{
				if (prop.CanRead)
					propMethods.Add(prop.GetGetMethod());
				if (prop.CanWrite)
					propMethods.Add(prop.GetSetMethod());
			}
			var interfaceMethods = intface.GetMethods().Except(propMethods).ToArray();

			foreach (var prop in properties)
			{
				var prp = context.Builder.DefineProperty(prop.Name, PropertyAttributes.None, prop.PropertyType, new Type[0]);
				if (prop.CanRead)
					prp.SetGetMethod(WrapMethod(typeToFindMatches, itcHolder, context, prop.GetGetMethod(), prepareActions));
				if (prop.CanWrite)
					prp.SetSetMethod(WrapMethod(typeToFindMatches, itcHolder, context, prop.GetSetMethod(), prepareActions));
			}
			foreach (var method in interfaceMethods)
			{
				WrapMethod(typeToFindMatches, itcHolder, context, method, prepareActions);
			}
		}

		private static MethodBuilder WrapMethod(Type typeToFindMatches,ItcHolderDescriptor itcHolder, TypeBuilderContext context, MethodInfo interfaceMethodsToImplement,  List<LambdaExpression> prepareActions)
		{
			FieldBuilder srcFld = context.Field(SRC_FLDNAME);
			FieldBuilder itcFld = context.Field(ITC_FLDNAME);

			var interfaceMethodParameters = interfaceMethodsToImplement.GetParameters();
			var intMeth = context.Builder.DefineMethod(interfaceMethodsToImplement.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot
													, interfaceMethodsToImplement.ReturnType
													, interfaceMethodParameters.Select(x => x.ParameterType).ToArray());

			if (interfaceMethodsToImplement.IsGenericMethod)
			{
				var genArgs = interfaceMethodsToImplement.GetGenericArguments();
				int pIndex = 0;
				GenericTypeParameterBuilder[] gParamDef = intMeth.DefineGenericParameters(genArgs.Select(x => "T" + ++pIndex).ToArray()).ToArray();
				for (int i = 0; i < genArgs.Length; i++)
				{
					gParamDef[i].SetBaseTypeConstraint(genArgs[i].BaseType);
					gParamDef[i].SetInterfaceConstraints(genArgs[i].GetInterfaces());
				}
			}

			for (int i = 0; i < interfaceMethodParameters.Length; i++)
			{
				intMeth.DefineParameter(i + 1, interfaceMethodParameters[i].Attributes, interfaceMethodParameters[i].Name);
			}

			MethodsMapping itcMethodFindResult = FindInterceptionMethod(itcHolder, typeToFindMatches, interfaceMethodsToImplement);

			if (itcMethodFindResult == null)
			{
				SimpleWrapMethod(context, interfaceMethodsToImplement, srcFld, itcFld, intMeth);
			}
			else
			{
				GenerateDelegateCall(typeToFindMatches,context.Module, srcFld, itcFld, intMeth, itcMethodFindResult, prepareActions);
			}
			context.Builder.DefineMethodOverride(intMeth, interfaceMethodsToImplement);
			return intMeth;
		}

		private static void SimpleWrapMethod(TypeBuilderContext context, MethodInfo method, FieldBuilder srcFld, FieldBuilder itcFld, MethodBuilder intMeth)
		{
			var methodParameters = method.GetParameters();
			if (method.IsGenericMethod)
			{
				method = DefyneGenericInterfaceMethod(intMeth, method);
			}
			var methIL = intMeth.GetILGenerator();
			methIL.Emit(OpCodes.Ldarg_0);
			methIL.Emit(OpCodes.Ldfld, srcFld);
			for (int i = 0; i < methodParameters.Length; i++)
			{
				methIL.EmitLdarg((short)(i + 1));
			}
			methIL.Emit(OpCodes.Callvirt, method);
			methIL.Emit(OpCodes.Ret);
		}
		private static void GenerateDelegateCall(Type typeToFindMatches, ModuleBuilder module, FieldBuilder srcFld, FieldBuilder itcFld, MethodBuilder intMeth, MethodsMapping methodsMapping, List<LambdaExpression> prepareActions)
		{
			var delegateType = methodsMapping.InterceptorMethod.ContextDelegateType;

			if (delegateType.IsGenericType)
			{
				delegateType = DefyneGenericDelegateMethod(delegateType, srcFld.FieldType, methodsMapping.TargetMethod);
			}
			var decoratorGetTargetMethod = typeof (IDecorator).GetMethod("get_Target");
			var invokerType = InvokerFactory.GenerateInvoker(module, methodsMapping, delegateType, prepareActions);
			var invokerTypeInvokeMethod = invokerType.GetMethod("InvokeMethod");
			var invokerTypeDefineInvokeMethod = invokerType.GetMethod("DefineInvokerMethod");
			var invokerTypeCtor = invokerType.GetConstructors()[0];
			var invokerFields = invokerType.GetFields(BindingFlags.Public | BindingFlags.Instance);

			var itcMethod = methodsMapping.InterceptorMethod.Method.IsGenericMethod
								? DefyneGenericItcMethod(methodsMapping.InterceptorMethod.TargetType, methodsMapping.InterceptorMethod.Method, typeToFindMatches, methodsMapping.TargetMethod)
								: methodsMapping.InterceptorMethod.Method;


			if (invokerType.IsGenericType)
			{
				invokerType = invokerType.MakeGenericType(intMeth.GetGenericArguments());
				invokerTypeInvokeMethod = TypeBuilder.GetMethod(invokerType, invokerTypeInvokeMethod);
				invokerTypeCtor = TypeBuilder.GetConstructor(invokerType, invokerTypeCtor);
				invokerTypeDefineInvokeMethod = TypeBuilder.GetMethod(invokerType, invokerTypeDefineInvokeMethod);
				invokerFields = invokerFields.Select(x => TypeBuilder.GetField(invokerType, x)).ToArray();
			}

			var methIL = intMeth.GetILGenerator();
			methIL.DeclareLocal(invokerType);

			//=== CREATE AUTO INVOKER -> in steck new object
			methIL.Emit(OpCodes.Ldarg_0);
			methIL.Emit(OpCodes.Ldfld, srcFld);
			foreach (var pm in methodsMapping.ParametersMapping.Where(x => x.ItcParameter.IsBatch && x.IsLinkedItcParameter))
			{
				methIL.EmitLdarg((short)(pm.TargetParameterIndex + 1));
				if (pm.TargetParameter.ParameterType.IsByRef)
					methIL.EmitLdInd(pm.TargetParameter.ParameterType.GetElementType());

			}
			methIL.Emit(OpCodes.Newobj, invokerTypeCtor);
			//========================

			methIL.Emit(OpCodes.Stloc_0);

			methIL.Emit(OpCodes.Ldarg_0);
			methIL.Emit(OpCodes.Ldfld, itcFld);
			methIL.Emit(OpCodes.Ldarg_0);
			methIL.Emit(OpCodes.Callvirt, decoratorGetTargetMethod);

			if (methodsMapping.InterceptorMethod.InterceptorType == ItcType.Invoker)
				methIL.Emit(OpCodes.Ldloc_0);

			methIL.Emit(OpCodes.Ldloc_0);
			methIL.Emit(OpCodes.Ldftn, invokerTypeInvokeMethod);
			methIL.Emit(OpCodes.Newobj, delegateType.GetConstructors()[0]);

			if (methodsMapping.InterceptorMethod.InterceptorType == ItcType.Invoker)
			{
				methIL.Emit(OpCodes.Call, invokerTypeDefineInvokeMethod);
				methIL.Emit(OpCodes.Ldloc_0);
			}

			//=======
			int mapIndex = 0;
			while (mapIndex < methodsMapping.ParametersMapping.Length)
			{
				var map = methodsMapping.ParametersMapping[mapIndex];
				int capacity = CalculateCapacity(methodsMapping.ParametersMapping, map);
				if (map.ItcParameter.IsBatch)
				{
					if (map.ItcParameter.ParameterType == ItcParameterType.Ignore)
					{
						methIL.Emit(OpCodes.Ldnull);
					}
					else if (map.ItcParameter.ParameterType == ItcParameterType.Skip)
					{
						methIL.EmitLdc_I4(capacity);
						methIL.Emit(OpCodes.Newobj, map.ItcParameter.ParameterInfo.ParameterType.GetConstructors()[0]);
					}
					else
					{
						PutObjectArrayParametersArrayOntoStack(methIL, methodsMapping.TargetMethod, map.TargetParameterIndex, map.ItcParameter.BatchType, capacity);
						methIL.Emit(OpCodes.Newobj, map.ItcParameter.ParameterInfo.ParameterType.GetConstructors()[0]);
					}
				}
				else
				{
					methIL.EmitLdarg((short)(map.TargetParameterIndex + 1));
					if (map.TargetParameter.ParameterType.IsValueType &&
						!map.ItcParameter.ParameterInfo.ParameterType.IsGenericParameter &&
						!map.ItcParameter.ParameterInfo.ParameterType.IsValueType)
						methIL.Emit(OpCodes.Box, map.TargetParameter.ParameterType);
				}
				mapIndex += capacity != 0 ? capacity : 1;
			};

			methIL.Emit(OpCodes.Callvirt, itcMethod);
			if (methodsMapping.TargetMethod.ReturnType == typeof(void) && itcMethod.ReturnType != typeof(void))
				methIL.Emit(OpCodes.Pop);
			// set out params
			foreach (var paramMap in methodsMapping.ParametersMapping.Where(x => x.ItcParameter.IsBatch && x.IsLinkedItcParameter && x.TargetParameter.ParameterType.IsByRef))
			{
				methIL.EmitLdarg((short)(paramMap.TargetParameterIndex + 1));
				methIL.Emit(OpCodes.Ldloc_0);
				var field = invokerFields.Single(x => x.Name == paramMap.TargetParameter.Name);
				methIL.Emit(OpCodes.Ldfld, field);
				methIL.EmitStInd(field.FieldType);
			}

			methIL.Emit(OpCodes.Ret);
		}


		private static MethodInfo DefyneGenericInterfaceMethod(MethodBuilder intMeth, MethodInfo method)
		{
			return method.GetGenericMethodDefinition().MakeGenericMethod(intMeth.GetGenericArguments());
		}

		private static void GenerateConstructor(TypeBuilderContext typeContext)
		{
			typeContext.DefineConstructor(
				new[] { typeContext.Field(SRC_FLDNAME).FieldType, typeContext.Field(ITC_FLDNAME).FieldType },
				(type, ctrIL) =>
				{
					ctrIL.Emit(OpCodes.Ldarg_0);
					ctrIL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
					ctrIL.Emit(OpCodes.Ldarg_0);
					ctrIL.Emit(OpCodes.Ldarg_1);
					ctrIL.Emit(OpCodes.Stfld, type.Field(SRC_FLDNAME));
					ctrIL.Emit(OpCodes.Ldarg_0);
					ctrIL.Emit(OpCodes.Ldarg_2);
					ctrIL.Emit(OpCodes.Stfld, type.Field(ITC_FLDNAME));
					ctrIL.Emit(OpCodes.Ret);
				});
		}
		private static TypeBuilderContext CreateType(TypeDescription sourceType)
		{
			var context = TypeBuilderContext.DefineType(
					String.Format("{0}.{1}<decorator{2}>",
							  sourceType.Definition.Namespace, sourceType.Definition.Name,
							  Helpers.GenerateNewUniqueIdentiricator()));

			context.DefineInterfaceImplementation(typeof(IDecorator));
			context.DefineInterfaceImplementation(sourceType.Interfaces);

			return context;
		}
	}
}
